package com.softserveinc.ita.kaiji.model.util;

import org.apache.log4j.Logger;

import java.util.*;

/**
 * Universal finite-state machine.
 * <link>http://en.wikipedia.org/wiki/Finite-state_machine</link>
 * Recommended implement interface <code>Statable</code> for class with using state machine
 * @see com.softserveinc.ita.kaiji.model.util.Statable
 *
 * State machine using examples:
 * @see com.softserveinc.ita.kaiji.model.game.Game
 * @see com.softserveinc.ita.kaiji.model.game.Round
 * @see com.softserveinc.ita.kaiji.model.player.Player
 *
 * @author Paziy Evgeniy
 * @version 2.5
 * @since 19.03.14
 */
public class FiniteStateMachine<T> {

    private static final Logger LOGGER = Logger.getLogger(FiniteStateMachine.class);
    private static final int DELTA_INDEX_FOR_PREVIOUS_STATE = 2;

    /**
     * Key of Map is a state from which we wont to switch.
     * Value of Map is a Set of states to which we can switch.
     */
    private final Map<T, Set<T>> stateSchema;

    private volatile T currentState;

    //todo concurrent List
    private List<T> statusHistory = new ArrayList<>();

    public FiniteStateMachine(T startState, Map<T, Set<T>> stateSchema) {
        currentState = startState;
        this.stateSchema = Collections.unmodifiableMap(stateSchema);

        statusHistory.add(startState);
    }

    /**
     * Returns current state of machine.
     * @return current state of machine
     */
    public T getCurrentState() {
        return currentState;
    }

    /**
     * Returns state previous to current state of machine.
     * Return null if there was no one switch of state.
     * @return previous state
     */
    public T getPreviousState() {
        if (statusHistory.size() < DELTA_INDEX_FOR_PREVIOUS_STATE) {
            return null;
        }

        return statusHistory.get(statusHistory.size() - DELTA_INDEX_FOR_PREVIOUS_STATE);
    }

    /**
     * Full status history of finite state machine.
     * @return list of states
     */
    public List<T> getStatusHistory() {
        return Collections.unmodifiableList(statusHistory);
    }

    /**
     * Returns unmodifiable Map of state schema.
     * @return unmodifiable Map of state schema
     */
    public Map<T, Set<T>> getStateSchema() {
        return stateSchema;
    }

    /**
     * Returns true if state can be switched to new state from current state.
     * @param newState state to which will machine switch
     * @return true if state can be switched
     */
    public boolean canSwitchTo(T newState) {
        boolean result;

        synchronized (this) {
            result = stateSchema.get(currentState).contains(newState);
        }

        if (LOGGER.isDebugEnabled()) {
            LOGGER.debug("Changing state for " + currentState + " to " + newState + " allowed: " + result);
        }

        return  result;
    }

    /**
     * Switch state of machine from current to new state.
     * @throws java.lang.IllegalStateException if machine can't be switched to new state
     * @param newState state to which will machine switch
     */
    public void switchState(T newState) {
        if (canSwitchTo(newState)) {
            if (LOGGER.isDebugEnabled()) {
                LOGGER.debug("State switched from: " + currentState + " to: " + newState);
            }

            synchronized (this) {
                currentState = newState;
                statusHistory.add(newState);
            }

        } else {
            String errorMessage = "Can't switch status from: " + currentState + " to: " + newState;
            IllegalStateException ex = new IllegalStateException(errorMessage);
            LOGGER.error("Switching state error", ex);
            throw ex;
        }
    }
}
